package com.trg.flex
{
	import com.trg.flex.remote.RemoteCall;
	
	import flash.utils.Dictionary;
	
	import mx.collections.ArrayCollection;
	
	/**
	 * <p>A ModelCache can keep an indexed client-side cache of VOs. The intention is to be able to keep a centralized store of some VOs on the client side for fast and easy access. When a VO class is pulled into the cache, every item of that type in the database is pulled. This works well for actively used and frequently changing data.
	 *
	 * <p>The best feature is that the cache makes sure it has exactly one copy of each VO in its store. If a new one comes in, it compares it by id and updates the old one rather than replacing it. This way, views that are bound to the data will also be updated.
	 *
	 * <p>NOTE: In the future, this might be expanded to automatically get updates when something changes on the server.
	 */
	public class ModelCache
	{
		private var indices:Dictionary = new Dictionary();
		private var lists:Dictionary = new Dictionary();
		private var status:Dictionary = new Dictionary();
		
		private static const LOADING:int = 1, LOADED:int = 2;
		
		private function getList(type:Class):ArrayCollection {
			if (typeof lists[type] == 'undefined') {
				lists[type] = new ArrayCollection();
			}
			return lists[type];
		}
		
		/**
		 * Returns true if the specified class is already fully loaded into the cache.
		 */
		public function isLoaded(type:Class):Boolean {
			return lists[type] == LOADED;
		}
		
		/**
		 * Returns true if the specified class is currently in the process of loading into the cache.
		 */
		public function isLoading(type:Class):Boolean {
			return lists[type] == LOADING;
		}
		
		/**
		 * Returns true if the specified class is already loaded or is being loaded into the cache.
		 */
		public function isLoadedOrLoading(type:Class):Boolean {
			return typeof lists[type] != 'undefined'
		}
		
		
		
		/**
		 * Return an ArrayCollection containing all the objects of the given type in the database. If the data is not loaded yet, it will be automatically loaded an the ArrayCollection will be updated dynamically. WARNING: Do not edit this ArrayCollection; it will mess up the Cache. Instead go through the cache to make changes.
		 */
		public function getAll(type:Class):ArrayCollection {
			if (!isLoadedOrLoading(type)) {
				refresh(type);
			}
			return getList(type);
		}
		
		/**
		 * Pull fresh data from the server for the given type. Trigger the callback when the refresh is complete.
		 */
		public function refresh(type:Class, callback:Callback=null):void {
			status[type] = LOADING;
			var call:RemoteCall = FrameworkConfig.entityDelegate.fetchAll(MetaDataUtils.getRemoteClass(type))
				.addResponder(new Callback(localRefresh, null, type, callback));
			call.description = 'Refreshing ' + MetaDataUtils.getSimpleName(type) + ' data';
			call.call();
		}
		
		/**
		 * Insert the given object into the server database and update the cache.
		 */
		public function create(entity:IValueObject, callback:Callback=null):void {
			var call:RemoteCall = FrameworkConfig.entityDelegate.create(entity)
				.addResponder(new Callback(localCreateOrUpdate, null, null, callback));
			call.description = "Saving...";
			call.longDescription = "Creating a " + MetaDataUtils.getSimpleName(entity);
			call.call();
		}
		
		/**
		 * Update the given object into the server database and update the cache.
		 */
		public function update(entity:IValueObject, callback:Callback=null):void {
			var call:RemoteCall = FrameworkConfig.entityDelegate.update(entity)
				.addResponder(new Callback(localCreateOrUpdate, null, null, callback));
			call.description = "Saving...";
			call.longDescription = "Saving changes to a " + MetaDataUtils.getSimpleName(entity);
			call.call();
		}
		
		/**
		 * Delete the object with the given id and type from the server database and update the cache.
		 */
		public function deleteById(id:int, type:Class, callback:Callback=null):void {
			var call:RemoteCall = FrameworkConfig.entityDelegate.deleteById(MetaDataUtils.getRemoteClass(type), id)
				.addResponder(new Callback(deleteCallback, null, {id : id, type : type}, callback));
			call.description = "Deleting...";
			call.longDescription = "Deleting a " + MetaDataUtils.getSimpleName(type);
			call.call();
		}
		
		/**
		 * Delete the given object from the server database and update the cache.
		 */
		public function deleteEntity(entity:IValueObject, callback:Callback=null):void {
			var call:RemoteCall = FrameworkConfig.entityDelegate.deleteEntity(entity)
				.addResponder(new Callback(deleteCallback, null, {id : entity['id'], type : MetaDataUtils.getType(entity)}, callback));
			call.description = "Deleting...";
			call.longDescription = "Deleting a " + MetaDataUtils.getSimpleName(entity);
			call.call();
		}
		
		/**
		 * Delete all the objects in the list from the server database and update the cache.
		 */
		public function deleteList(list:*, callback:Callback=null):void {
			var call:RemoteCall = FrameworkConfig.entityDelegate.deleteList(FlexUtils.collectionToArray(list))
				.addResponder(new Callback(deleteListCallback, null, list, callback));
			call.description = "Deleting...";
			call.longDescription = "Deleting one or more items.";
			call.call();
		}
		
		private function deleteCallback(ignore:*, data:Object):void {
			localDeleteById(data.id, data.type);
		}
		private function deleteListCallback(ignore:*, data:Object):void {
			localDeleteList(data);
		}
		
		
		
		/**
		 * Manually set the list of object of a given type in the cache without touching the server.
		 */
		public function localRefresh(list:*, type:Class):void {
			refreshList(FlexUtils.collectionToArray(list), type);
			status[type] = LOADED;
		}
		
		/**
		 * Manually create or update a VO in the cache without touching the server.
		 */
		public function localCreateOrUpdate(entity:IValueObject):void {
			saveItem(entity);
		}
		
		/**
		 * Manually delete the VO with the given id and type from the cache without touching the server.
		 */
		public function localDeleteById(id:int, type:Class):void {
			deleteItem(id, type);
		}
		
		/**
		 * Manually delete a VO from the cache without touching the server.
		 */
		public function localDeleteEntity(entity:IValueObject):void {
			deleteItem(entity['id'], MetaDataUtils.getType(entity));
		}
		
		/**
		 * Manually delete a list of VOs from the cache without touching the server.
		 */
		public function localDeleteList(list:*):void {
			var array:Array = FlexUtils.collectionToArray(list);
			for each (var entity:IValueObject in array) {
				localDeleteEntity(entity);
			}
		}
		
		
		
/* -------------------------------------------------------------------------
  The functions in this section are for updating the model when new data comes in from the server.
  We had to be careful to keep only one copy of each object so that if the object is changed,
  it is changed everywhere. For example, an Application will show up in the list of all applications
  and it will also show up in the ApplicationEvent list. So we process data that comes in and update
  the current objects with the changes or add new objects that come in. In the processing we reattach
  all the links on the objects to our existing objects. We keep track of our pool of objects through an
  indices on the ModelLocator. The indices are based on ids.  
*/

	   	/**
	   	 * Call this function when a single item is coming in from the server, either
	   	 * new or edited.
	   	 */
	   	private function saveItem(item:IValueObject):void {
	   		var type:Class = MetaDataUtils.getType(item);
	   		var collection:ArrayCollection = getList(type);
	   		var prev:* = indexGet(type, item['id']);
	   		
	   		if (prev != null) {
	   			//if found with same id, update
	   			editItem(item, prev);
	   		} else {
	   			//otherwise new, add to end of list	
				newItem(item);
	   		}
	   	}
	   	
	   	/**
	   	 * Call this function when a single item has been deleted
	   	 * NOTE: As far as I know this is not actually being used. All of our delete calls
	   	 * return a list of all remaining objects instead of the ids of those that were
	   	 * deleted.
	   	 */
	   	private function deleteItem(id:int, type:Class):void {
	   		var collection:ArrayCollection = getList(type);
	   		for (var i:int = 0; i < collection.length; i++){	   			
	   			if(id == collection[i].id){
	   				indexDelete(MetaDataUtils.getType(collection[i]), id);
	   				collection.removeItemAt(i);
	   				return;
	   			}	   			
	   		}   		
	   	}
	   	
	   	/**
	   	 * Call this function when a new list of all objects of a type is coming in from the server.
	   	 */
	   	private function refreshList(incoming:Array, type:Class):void {
	   		var item:*;
			var list:ArrayCollection = getList(type);
	   		
	   		if (list.length == 0 && incoming.length == 0) {
	   			//both lists are empty
	   			return;
	   		}
	   		
	   		
	   		//if incoming list is empty
	   		if (incoming.length == 0) {
	   			for each (item in list) {
	   				indexDelete(type, item.id);
	   			}
	   			list.source = [];
	   			return;
	   		}
	   		
	   		
	   		//make a copy of both lists so we can edit them without causing any trouble
	   		var toRemove:Array = list.source.concat();
	   		
	   		var i:int, prev:*;

	   		//process edited and new items. Eliminate items from the toRemove collection.
	   		for each (item in incoming) {
	   			prev = indexGet(type, item.id);
	   			if (prev != null) {
	   				//found
	   				editItem(item, prev);
	   				for (i = 0; i < toRemove.length; i++) {
	   					if (item.id == toRemove[i].id) {
	   						toRemove.splice(i, 1);
	   						break;
	   					}
	   				}
	   				
	   			} else {
	   				//not found
	   				newItem(item);
	   			}
	   		}
	   		
	   		//the remaining items in toRemove need to be removed
	   		for each (prev in toRemove) {
	   			for (i = list.length - 1; i >= 0; i--) {
	   				if (prev.id == list[i].id) {
	   					list.removeItemAt(i);
	   					break;
	   				}
	   			}
	   			indexDelete(type, prev.id);
	   		}
	   	}
	   	
	   	/**
	   	 * if the item is already in the index, return the version from the index;
	   	 * if it is not, add this version to the index and return it.
	   	 */
	   	private function getOrAdd(item:IValueObject):Object {
   			var prev:IValueObject = indexGet(MetaDataUtils.getType(item), item['id']);
   			if (prev != null) {
   				//if the item already exists, just use it
   				return prev;
   			} else {
   				//if not, make this a new type
   				newItem(item);
   				return item;
   			}	   		
	   	}
	   	
	   	/**
	   	 * replace an item that is already there
	   	 */
	   	private function editItem(item:Object, prev:Object):void {
   			//make sure not to create a new copy of an existing item that this object has
   			//(ex: probe objects have a type item, OwnerProcess objects have an owner item, etc.)
   			var fkeys:Object = MetaDataUtils.getForeignKeys(item);
   			if(fkeys != null) {
   				for (var p:String in fkeys) {
   					if (item[p] != null) {
		   				if (prev[p] != null && item[p].id == prev[p].id) {
		   					//if the linked item id has not changed, use the linked item from prev
		   					item[p] = prev[p];
		   				} else {
		   					//if the linked item has a different id, get the item from the index or add it
		   					item[p] = getOrAdd(item[p]);
		   				}
		   			}
   				}
   			}

   			FlexUtils.shallowCopy(item, prev);
	   	}
	   	
	   	/**
	   	 * add an item that we know is not there yet
	   	 */
	   	private function newItem(item:IValueObject):void {
   			var type:Class = MetaDataUtils.getType(item);
   			indexAdd(type, item);
   			
   			//make sure not to create a new copy of an existing item that this object has
   			//(ex: probe objects have a type item, OwnerProcess objects have an owner item, etc.) 
   			var fkeys:Object = MetaDataUtils.getForeignKeys(type);
   			if(fkeys != null) {
   				for (var p:String in fkeys) {
   					if (item[p] != null) {
   						var ftype:String = fkeys[p];
 	   					//get the linked item from the index or add it
	   					item[p] = getOrAdd(item[p]);  						
   					}
   				}
   			}
   			
   			getList(type).addItem(item);
	   	}
	   	
	   	/**
	   	 * get the item from the given index with the given id. Returns null if not found.
	   	 */
	   	private function indexGet(type:Class, id:int):IValueObject {
	   		var index:Dictionary = indices[type];
	   		if (index == null) return null;
	   		return index[id];
	   	}
	   	
	   	/**
	   	 * inserts the item into the given index
	   	 */
	   	private function indexAdd(type:Class, item:IValueObject):void {
	   		var index:Dictionary = indices[type];
	   		if (index == null) {
	   			index = new Dictionary();
	   			indices[type] = index;
	   		}
	   		index[item['id']] = item;
	   	}
	   	
	   	/**
	   	 * removes the item with the given id from the given index
	   	 */
	   	private function indexDelete(type:Class, id:int):void {
	   		var index:Dictionary = indices[type];
	   		if (index == null) return;
	   		delete index[id];
	   	}
		
		
	}
}